package com.ar4j.bench.test.synthetic;

import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.sql.DataSource;

import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.util.StopWatch;

import com.ar4j.bench.test.util.TestUtils;

/**
 * A utility that is able to run a configured crud benchmark. Callbacks are used to perform actual operations on the backing store
 * while this class simply coordinates a benchmark.
 */
public class SyntheticCrudBenchmark<T> {
  private StopWatch watch;
  private int batchSize;
  private DataSource dataSource;
  private PlatformTransactionManager txManager;
  private boolean transactional;
  private boolean transactionPerOperation;
  private SyntheticCrudBenchmarkCallbacks<T> callbacks;
  
  /**
   * Run through full benchmark. Creates test objects, inserts them in the backing store, performs random seek benchmark,
   * performs random update benchmark, performs random remove benchmark.
   */
  public void bench() {
    
    // validate
    assertNotNull("Should have a stopwatch", watch);
    assertTrue("Batch size should be greater than 0", batchSize > 0);
    assertNotNull("Should have a data source", dataSource);
    
    if(transactional) {
      assertNotNull("Need a transaction manager for transactional runs", txManager);
    }
    
    assertNotNull("Should have a callbacks instance", callbacks);
    
    resetDb();
    prepareForRun();
    List<T> objects = generateObjects();
    long[] ids = insertObjects(objects);
    randomSeekBenchmark(ids, objects);
    randomUpdateBenchmark(objects);
    randomRemoveBenchmark(ids);
    cleanup();
    
  }

  public StopWatch getWatch() {
    return watch;
  }

  public SyntheticCrudBenchmark<T> setWatch(StopWatch watch) {
    this.watch = watch;
    return this;
  }

  public int getBatchSize() {
    return batchSize;
  }

  public SyntheticCrudBenchmark<T> setBatchSize(int batchSize) {
    this.batchSize = batchSize;
    return this;
  }

  public DataSource getDataSource() {
    return dataSource;
  }

  public SyntheticCrudBenchmark<T> setDataSource(DataSource dataSource) {
    this.dataSource = dataSource;
    return this;
  }

  public PlatformTransactionManager getTxManager() {
    return txManager;
  }

  public SyntheticCrudBenchmark<T> setTxManager(PlatformTransactionManager txManager) {
    this.txManager = txManager;
    return this;
  }

  public boolean isTransactional() {
    return transactional;
  }

  public SyntheticCrudBenchmark<T> setTransactional(boolean transactional) {
    this.transactional = transactional;
    return this;
  }

  public boolean isTransactionPerOperation() {
    return transactionPerOperation;
  }

  public SyntheticCrudBenchmark<T> setTransactionPerOperation(boolean transactionPerOperation) {
    this.transactionPerOperation = transactionPerOperation;
    return this;
  }

  public SyntheticCrudBenchmarkCallbacks<T> getCallbacks() {
    return callbacks;
  }

  public SyntheticCrudBenchmark<T> setCallbacks(SyntheticCrudBenchmarkCallbacks<T> callbacks) {
    this.callbacks = callbacks;
    return this;
  }
  
  private void resetDb() {
    watch.start("Resetting the DB");
    TestUtils.runDatabaseResetScript(dataSource);
    watch.stop();
  }
  
  private void prepareForRun() {
    watch.start("Prepare for run");
    callbacks.prepare();
    watch.stop();
  }
  
  private List<T> generateObjects() {
    watch.start("Generating batch");
    List<T> objects = new ArrayList<T>(batchSize);
    for(int i = 0; i < batchSize; i++) {
      objects.add(callbacks.generate());
    }
    watch.stop();
    
    return objects;
  }
  
  private long[] insertObjects(List<T> objects) {
    watch.start("Insert");
    long[] ids = new long[objects.size()];
    
    TransactionStatus outer = null;
    if(transactional) { outer = txManager.getTransaction(new DefaultTransactionDefinition(TransactionDefinition.PROPAGATION_REQUIRED)); }
    for(int i = 0; i < objects.size(); i++) {
      TransactionStatus inner = null;
      if(transactionPerOperation) { inner = txManager.getTransaction(new DefaultTransactionDefinition(TransactionDefinition.PROPAGATION_REQUIRES_NEW)); }
    
      ids[i] = callbacks.add(objects.get(i));
      
      if(inner != null) { txManager.commit(inner); }
    }
    
    if(outer != null) { txManager.commit(outer); }
    watch.stop();
    return ids;
  }
  
  private void randomSeekBenchmark(long[] ids, List<T> objects) {
    watch.start("Retrieve objects in random order");
    TestUtils.shuffleLongArray(ids);
    
    TransactionStatus outer = null;
    if(transactional) { outer = txManager.getTransaction(new DefaultTransactionDefinition(TransactionDefinition.PROPAGATION_REQUIRED)); }
    for(int i = 0; i < ids.length; i++) {
      TransactionStatus inner = null;
      if(transactionPerOperation) { inner = txManager.getTransaction(new DefaultTransactionDefinition(TransactionDefinition.PROPAGATION_REQUIRES_NEW)); }
      
      T obj = callbacks.findById(ids[i]);
      assertNotNull("All objects should be retrievable", obj);
      objects.set(i, obj);
      
      if(inner != null) { txManager.commit(inner); }
    }
    
    if(outer != null) { txManager.commit(outer); }
    watch.stop();
  }
  
  private void randomUpdateBenchmark(List<T> objects) {
    watch.start("Update objects in random order ");
  
    Collections.shuffle(objects);
    TransactionStatus outer = null;
    if(transactional) { outer = txManager.getTransaction(new DefaultTransactionDefinition(TransactionDefinition.PROPAGATION_REQUIRED)); }
    for(int i = 0; i < objects.size(); i++) {
      TransactionStatus inner = null;
      if(transactionPerOperation) { inner = txManager.getTransaction(new DefaultTransactionDefinition(TransactionDefinition.PROPAGATION_REQUIRES_NEW)); }
      
      T obj = objects.get(i);
      callbacks.randomize(obj);
      callbacks.update(obj);
      
      if(inner != null) { txManager.commit(inner); }
    }
    
    if(outer != null) { txManager.commit(outer); }
    watch.stop();
  }
  
  private void randomRemoveBenchmark(long[] ids) {
    watch.start("Remove objects in random order");
    
    TestUtils.shuffleLongArray(ids);
    TransactionStatus outer = null;
    if(transactional) { outer = txManager.getTransaction(new DefaultTransactionDefinition(TransactionDefinition.PROPAGATION_REQUIRED)); }  
    for(int i = 0; i < ids.length; i++) {
      TransactionStatus inner = null;
      if(transactionPerOperation) { inner = txManager.getTransaction(new DefaultTransactionDefinition(TransactionDefinition.PROPAGATION_REQUIRES_NEW)); }
      
      callbacks.removeById(ids[i]);
      
      if(inner != null) { txManager.commit(inner); }
    }
    
    if(outer != null) { txManager.commit(outer); }
    watch.stop();
  }
  
  private void cleanup() {
    watch.start("Cleanup");
    callbacks.cleanup();
    watch.stop();
  }

}
